
在上一篇我們談過 Shared Database 與 Database per Service 的設計。結論是:在微服務架構中,Database per Service 雖然帶來了自治性與彈性,但也同時打破了傳統「跨表 Join」的便利。
換句話說,原本只要一條 SQL 就能完成的查詢,現在可能需要跨越多個微服務、甚至多種資料庫,這對開發與系統設計帶來了新的挑戰。(我今年好像一直在這個循環裡面繞 …)
今天,我們就來探討兩個常見的解決模式:
- API Composition —— 以 API 為單位組合查詢結果
- CQRS (Command Query Responsibility Segregation) —— 用專門的讀庫支援跨服務查詢
問題背景:跨服務查詢的難題
假設我們有一個線上商店系統,它包含下列微服務:
- Customer Service:管理客戶資訊(信用額度、基本資料)
- Order Service:管理訂單資訊(訂單清單、消費金額)
- Product Service:管理商品目錄與庫存
當前端要顯示「客戶詳細資訊」時,通常需要以下資料:
- 客戶的基本資料(Customer Service)
- 客戶的訂單歷史(Order Service)
- 訂單中商品的詳細資訊(Product Service)
在傳統單體式應用中,我們只要寫一個 SQL Join:
SELECT c.customer_id, c.name, o.order_id, p.product_name, p.price
FROM customer c
JOIN orders o ON c.customer_id = o.customer_id
JOIN products p ON o.product_id = p.product_id
WHERE c.customer_id = 123;
但在微服務架構中,資料分散在三個不同服務、三個不同資料庫,這樣的 SQL 查詢不再可能。
API Composition 模式
API Composition 是一種 透過 API 組合不同微服務的查詢結果 的設計模式。
它的角色分成兩類:
- API 組合器(API Composer):通常是 API Gateway 或 BFF (Backend for Frontend),負責統一發送請求並組合結果。
- 服務提供者(Service Provider):各個微服務本身,提供查詢 API。
以「客戶詳細資訊查詢」為例:
- API Gateway 收到前端請求 /customer/123/details
- API Gateway 依序呼叫:
2.1. Customer Service → GET /customers/123
2.2. Order Service → GET /customers/123/orders
2.3. Product Service → GET /products/{id}
- API Gateway 將三個服務的回應組合成一個 JSON,回傳給前端。
結果可能是這樣:
{
"customer": {
"id": 123,
"name": "Alice",
"creditLimit": 5000
},
"orders": [
{
"orderId": 1001,
"total": 200,
"items": [
{ "productName": "iPhone 15", "price": 150 },
{ "productName": "Phone Case", "price": 50 }
]
}
]
}
API Composition 的優點
-
簡單直觀:不需額外維護讀庫或事件系統,開發門檻低。
-
即時性:每次查詢都取得最新資料,無需考慮同步延遲。
-
靈活性:可快速組合多個服務資料,適合中小型查詢。
API Composition 的限制與缺點
-
效能問題:需要多次網路呼叫,若一個頁面需要查 5 個服務,可能會很慢。
-
可用性風險:只要一個服務失效,整個查詢就失敗。
-
缺乏交易一致性:不同服務的資料可能不是同一個時間點的狀態 (隱含著 Saga Pattern 必須被實作)。
整理上述的內容,可以歸納在「查詢資料筆數少」、「即時性比一致性更重要」的情境可採用此模式。
CQRS 模式
CQRS(Command Query Responsibility Segregation,指令與查詢責任分離)是一種 將「寫」與「讀」分離 的設計模式。
- Command(寫):由各微服務負責,資料進入專屬的資料庫。
- Query(讀):由專門的「查詢庫」處理,該查詢庫會透過 事件(Event) 來保持更新。
一樣以「客戶詳細資訊查詢」為例:
- Customer Service、Order Service、Product Service 各自維護自己的資料庫。
- 當資料變動時(新增訂單、修改商品),它們會發布 Domain Event:
2.1. CustomerUpdated
2.2. OrderCreated
2.3. ProductUpdated
- 一個專門的 Query Service(或查詢資料庫)訂閱這些事件,並將資料存到一個「查詢資料庫」(通常是 NoSQL,如 MongoDB、ElasticSearch)。
- 當前端需要查詢「客戶詳細資訊」時,直接查詢 Query DB,而不是打到多個微服務。
優點:
- 高效能:跨服務查詢只需打一次資料庫,不用多次 API 呼叫。
- 可擴展性:可以針對查詢需求設計專門的讀庫,例如 ElasticSearch 支援全文檢索。
- 靈活報表:適合需要跨大量資料的查詢或 BI 報表。
缺點
- 資料同步延遲:事件傳遞有時間差,查詢資料可能不是最新的。
- 架構複雜度高:需要事件機制(Kafka, RabbitMQ),並維護額外的查詢庫。
- 開發成本高:需要設計額外的資料同步與一致性處理。
整理上述的內容,可以歸納在「報表系統」、「BI 分析」、「批次程式」採用這個模式。
API Composition vs CQRS
| 維度 |
API Composition |
CQRS |
| 查詢方式 |
由 API Gateway/BFF 發送多次請求,組合結果 |
由查詢庫(Read DB)一次查詢 |
| 即時性 |
最新資料 |
可能有延遲(最終一致性) |
| 效能 |
跨多服務呼叫,延遲較高 |
高效能,查詢一次即可 |
| 適用場景 |
小量資料、即時性高 |
報表、大量查詢、跨服務數據整合 |
| 複雜度 |
簡單 |
複雜(需事件驅動架構) |
| 一致性 |
無交易一致性保證 |
最終一致性 |
結語
在微服務架構下,跨資料庫查詢 是一個必然的挑戰。
- API Composition:適合簡單、少量的查詢需求,即時但效能有限。
- CQRS:適合大量、複雜的查詢,效能強大,但需要付出同步延遲與架構複雜度的代價。
一個成熟的微服務架構,往往 兩者並用:
- 前台查詢 → API Composition
- 後台報表 → CQRS
最終,選擇的關鍵在於 需求優先順序:是要即時,還是要高效能?